// Copyright (c) 2011 Blue Onion Software, All rights reserved
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web;
using System.Xml;

namespace BlueOnionSoftware.Bloget
{
    internal static class PingBackClient
    {
        internal static void PingLinks(Bloget bloget, string sourceLink, string text)
        {
            Throw.IfNull(bloget, "bloget");
            Throw.IfNullOrEmpty(sourceLink, "sourceLink");
            Throw.IfNull(text, "text");

            if (Uri.IsWellFormedUriString(sourceLink, UriKind.Absolute) == false)
            {
                Log.PingFailed(bloget, sourceLink, "link is not well-formed");
            }

            var site = new PingSite(bloget, sourceLink, text);

            if (ThreadPool.QueueUserWorkItem(PingProc, site) == false)
            {
                Log.PingBackFailed(bloget, "PingLinks", sourceLink, "QueueUserWorkItem failed to create thread");
            }
        }

        internal class PingSite
        {
            private readonly Bloget bloget;
            private readonly string sourceLink;
            private readonly string text;

            internal PingSite(Bloget bloget, string sourceLink, string text)
            {
                this.bloget = bloget;
                this.sourceLink = sourceLink;
                this.text = text;
            }

            internal Bloget Bloget
            {
                get { return bloget; }
            }

            internal string SourceLink
            {
                get { return sourceLink; }
            }

            internal string Text
            {
                get { return text; }
            }
        }

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private static void PingProc(object site)
        {
            try
            {
                var pingSite = (PingSite) site;
                var source = new Uri(pingSite.SourceLink);

                var hrefs = new Regex("href\\s*=\\s*(?:\"(?<1>[^\"]*)\"|(?<1>\\S+))", RegexOptions.IgnoreCase);
                var matches = hrefs.Matches(pingSite.Text);

                foreach (Match match in matches)
                {
                    var targetLink = match.Groups[1].Value;

                    if (Uri.IsWellFormedUriString(targetLink, UriKind.Absolute) == false)
                    {
                        Log.PingFailed(pingSite.Bloget, targetLink, "target link is not well formed");
                        continue;
                    }

                    var target = new Uri(targetLink);
                    string serverLink;

                    try
                    {
                        serverLink = PingBackAutoDiscover(target);
                    }

                    catch (WebException we)
                    {
                        Log.PingFailed(pingSite.Bloget, targetLink, "WebException during auto discover:" + we.Message);
                        continue;
                    }

                    if (string.IsNullOrEmpty(serverLink) == false &&
                        Uri.IsWellFormedUriString(serverLink, UriKind.Absolute))
                    {
                        var server = new Uri(serverLink);

                        try
                        {
                            Ping(pingSite, server, source, target);
                        }

                        catch (WebException we)
                        {
                            Log.PingFailed(pingSite.Bloget, serverLink,
                                           "WebException from pingback server:" + we.Message);
                            continue;
                        }
                    }

                    else
                    {
                        Log.PingFailed(pingSite.Bloget, targetLink, "No pingback server found");
                    }
                }
            }

            catch (Exception ex)
            {
                Log.Exception(null, "PingProc", ex);
            }
        }

        private static string PingBackAutoDiscover(Uri link)
        {
            Throw.IfNull(link, "link");
            var request = (HttpWebRequest) WebRequest.Create(link);

            using (var response = (HttpWebResponse) request.GetResponse())
            {
                if (response.StatusCode == HttpStatusCode.OK)
                {
                    var server = response.GetResponseHeader("X-Pingback");

                    if (string.IsNullOrEmpty(server) == false)
                    {
                        return server;
                    }

                    using (var reader = new StreamReader(response.GetResponseStream()))
                    {
                        var buffer = new char[0x1400]; // specification says first 5K only
                        var count = reader.Read(buffer, 0, buffer.Length);
                        var content = new string(buffer, 0, count);

                        if (string.IsNullOrEmpty(content) == false)
                        {
                            var linkElement =
                                new Regex("<link rel=\"pingback\" href\\s*=\\s*(?:\"(?<1>[^\"]*)\"|(?<1>\\S+))",
                                          RegexOptions.IgnoreCase);

                            var match = linkElement.Match(content);

                            if (match.Success)
                            {
                                return HttpUtility.HtmlDecode(match.Groups[1].Value);
                            }
                        }
                    }
                }
            }

            return null;
        }

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private static void Ping(PingSite pingSite, Uri server, Uri source, Uri target)
        {
            Throw.IfNull(source, "source");
            Throw.IfNull(target, "target");

            try
            {
                var request = (HttpWebRequest) WebRequest.Create(server);
                request.Method = "POST";
                request.UserAgent = "Bloget/1.0";
                request.ContentType = "text/xml";

                using (var memoryStream = new MemoryStream())
                {
                    PingBackRequest(memoryStream, source, target);
                    var buffer = memoryStream.ToArray();
                    request.ContentLength = buffer.Length;
                    request.GetRequestStream().Write(buffer, 0, buffer.Length);
                }

                using (var response = (HttpWebResponse) request.GetResponse())
                {
                    if (response.StatusCode == HttpStatusCode.OK)
                    {
                        using (var stream = new StreamReader(response.GetResponseStream()))
                        {
                            var xmlSettings = new XmlReaderSettings();
                            xmlSettings.IgnoreComments = true;
                            xmlSettings.IgnoreProcessingInstructions = true;
                            xmlSettings.IgnoreWhitespace = true;

                            using (var reader = XmlReader.Create(stream, xmlSettings))
                            {
                                reader.Read();
                                reader.ReadStartElement("methodResponse");

                                if (reader.Name == "params")
                                {
                                    reader.ReadStartElement("params");
                                    reader.ReadStartElement("param");
                                    reader.ReadStartElement("value");
                                    var reply = reader.ReadElementString("string");
                                    reply = reply.Substring(0, Math.Min(reply.Length, 100));
                                    reply = reply.Replace('\r', ' ');
                                    reply = reply.Replace('\n', ' ');
                                    Log.PingSucceeded(pingSite.Bloget, target.ToString(), reply);
                                }

                                else
                                {
                                    reader.ReadStartElement("fault");
                                    reader.ReadStartElement("value");
                                    reader.ReadStartElement("struct");
                                    reader.ReadStartElement("member");
                                    reader.ReadElementString("name");
                                    reader.ReadStartElement("value");
                                    reader.Read();

                                    Log.PingFailed(pingSite.Bloget, target.ToString(),
                                                   reader.ReadContentAsString());
                                }
                            }
                        }
                    }

                    else
                    {
                        Log.PingFailed(pingSite.Bloget, target.ToString(),
                                       "Http Response Code = " + response.StatusCode);
                    }
                }
            }

            catch (Exception ex)
            {
                Log.PingFailed(pingSite.Bloget, server.ToString(), "Ping exception: " + ex.Message);
            }
        }

        internal static void PingBackRequest(Stream stream, Uri source, Uri target)
        {
            Throw.IfNull(stream, "stream");
            Throw.IfNull(source, "source");
            Throw.IfNull(target, "target");

            var writerSettings = new XmlWriterSettings();
            writerSettings.Indent = true;

            using (var writer = XmlWriter.Create(stream, writerSettings))
            {
                writer.WriteStartDocument();
                writer.WriteStartElement("methodCall");
                writer.WriteElementString("methodName", "pingback.ping");

                writer.WriteStartElement("params");
                writer.WriteStartElement("param");
                writer.WriteStartElement("value");
                writer.WriteElementString("string", source.ToString());
                writer.WriteEndElement();
                writer.WriteEndElement();

                writer.WriteStartElement("param");
                writer.WriteStartElement("value");
                writer.WriteElementString("string", target.ToString());

                writer.WriteEndDocument();
            }
        }
    }
}